김사람 블로그
thumbnail
웹워커 타이머(Web Worker Timer)
React / TS
2022.08.22.

Timer 제작 과정

setInterval → useInterval → Web Worker

TimeLog에 사용할 타이머를 만드는 과정에서 겪은 경험을 공유하고자 합니다.

처음은 누구라도 그러하듯이 setInterval로 타이머를 만들기 시작했습니다.

하지만 타이머가 제대로 작동하지 않아 검색을 해보니, setInterval에 몇 가지 문제가 있는데,

그 해결책으로 useInterval이라는 custom Hook을 발견했습니다. (문제에 관한 내용은 아래 참고 링크에 있습니다)

따라서 useInterval로 바꾸었고, 타이머가 작동하여 뿌듯한 기분으로 다른 작업을 하였습니다.

하지만 저의 뿌듯함을 비웃듯 타이머에 또 다른 문제가 생겼습니다.

타이머를 오래 작동하면 타이머에 지연이 생기는 것이었습니다.

이번에도 검색을 통해 문제의 원인을 찾아보았습니다.

  • CPU가 과부하 상태인 경우
  • 브라우저 탭이 백그라운드 모드인 경우
  • 노트북이 배터리에 의존해서 구동 중인 경우

위의 경우 브라우저 내 타이머가 느려져 setInterval의 지연 간격이 길어진다는 것이었습니다. (useInterval도 setInterval을 사용합니다)

경우 추측

크롬은 메모리가 부족하면 비활성 탭이 절전 되는 기능인, “Automatic tab discarding”이 내장되어 있습니다.

“chrome://flags”에서 Automatic tab discarding을 조절할 수 있었지만 없어졌고,

“chrome://discards”에 접속하면 모든 탭에 Auto Discardable이 활성화 되어 있는 것을 확인할 수 있습니다.

(참고로 Graph탭에서 Web Workers를 확인할 수도 있습니다)

이 기능으로 인해 탭이 절전 됐거나, 다른 내장 기능에 의해 자동으로 백그라운드 모드에 진입해서 지연된 것 같습니다.


이를 해결하기 위한 무수한 검색 중에 Web Worker를 발견합니다.

Web Worker는 웹의 Main thread와 별개로, Background thread에서 script를 실행하는 기술입니다.

따라서 Web Worker script에서 setInterval을 사용하면 브라우저 내 타이머 지연과 상관없이 정상 작동합니다.

그러니 어서 사용합시다.


React & TypeScript
// tsconfig.json

{
  "compilerOptions": {
    ...
    "lib": [ ... "WebWorker"], // Worker Scope에서 쓰이는 타입 추가
    ...
    }
}
// worker.ts

const self = globalThis as unknown as DedicatedWorkerGlobalScope; // Double assertion
let time = 0;

self.onmessage = () => {
  setInterval(() => {
    time++;
    self.postMessage(time);
  }, 100);
};

export {}; // --isolatedModules 에러 방지 (모듈화)
// Timer.tsx

function Timer() {
  const [time, setTime] = useState(0);
  const [timerOn, setTimerOn] = useState(false);
  const timeSecond = Math.floor(time / 10);

  const timerStart = () => {
    setTimerOn(true);
  };
  const timerStop = () => {
    setTimerOn(false);
  };

  useEffect(() => {
    const worker = new Worker(new URL('worker/worker', import.meta.url)); // webpack5 이후 용법
    if (timerOn === true) {
      worker.postMessage('timer start');
      worker.onmessage = (e: MessageEvent<string>) => {
        setTime(time + Number(e.data));
      };
    }
    return () => {
      worker.terminate();
    };
  }, [timerOn]);

  return (
    <div>
      <div className='timer'>{timeSecond}</div>
      <div>
        <button onClick={timerStart}>Start</button>
        <button onClick={timerStop}>Stop</button>
      </div>
    </div>
  );
}

export default Timer;

동작 과정

  1. Background thread에서 실행할 Worker sciprt를 worker.ts에 작성합니다. (이하 worker)

  2. Timer.tsx(main script)에서 new Worker()로 worker를 실행합니다.

    • timerOn 값이 바뀔 때마다 그 전에 worker.terminate()로 현재 worker가 종료되고, 이후 새로운 worker를 실행합니다.

  3. 타이머 버튼의 클릭 이벤트로 timerOn 값이 true가 되면, worker.postMessage()로 worker에 메세지 데이터를 전달합니다.

    • 여기서 메세지 데이터는 사용하지 않으므로 중요하지 않습니다.

  4. worker의 self.onmessage에 적힌 동작들이 실행됩니다. (setInterval 실행)

  5. Timer.tsx에서 worker.onmessage로, worker에서 self.postMessage()로 보낸 time을 100ms마다 기존 time에 추가합니다.


참고 링크

서비스워커 알림(Service Worker Notification)
← 다음 글
커스텀 셀렉트 박스(Custom Select Box)
이전 글 →
We can love completely without complete understanding.
- Norman Maclean, A River Runs Through it.